import fs from "fs-extra";
import os from "os";
import { CmdOption } from "../CmdOption.js";
import { toolchain } from "../toolchain.js";
import { sendBuildFailureAlert } from "./alert.js";
import { Cmd, ExecuteError } from "./Cmd.js";
import path from "path";
import { CommonEnv } from "../common/CommonEnv.js";
import { eitherPath, readVersionTxt } from "./vendor.js";
import { BuildParams } from "../BuildParams.js";
import { compareVersions } from "compare-versions";
import { env } from "../env.js";
import { PreBuildAppTask, PrebuildAppData } from "../common/tasks/PreBuildAppTask.js";
import { WebGLEnv } from "../webgl/WebGLEnv.js";

interface IUnityEditor {
    version: string
    manual?: boolean
    architecture?: string
    location: string[]
}

interface IHubEditors {
    [version: string]: IUnityEditor
}

interface IHubEditorsV2 {
    schema_version: string,
    data: IUnityEditor[]
}

export const enum EPuertsPackage {
    Core = 'com.tencent.puerts.core',
    CommonJS = 'com.tencent.puerts.commonjs',
    WebGL = 'com.tencent.puerts.webgl'
}

export class UnityHelper {
    private fullVersion?: string;

    private _lastErrCode = 0;
    public get lastErrCode(): number {
        return this._lastErrCode;
    }

    private _env: {
        dependencies: Record<string, string>,
        puertsCoreVer: string,
        puertsWebglVer: string,
    } | null = null;

    // 为减少日志，建议使用以下语句避免输出堆栈
    // Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Hello World!");
    public async runUnityCommand(cmdOption: CmdOption, params: string[], tag = 'normal', ignoreError?: boolean, outputStripper?: (raw: string) => string): Promise<Cmd> {
        let cmd = new Cmd();
        this._lastErrCode = 0;
        try {
            if (toolchain.params.channelCfg.buildParams) {
                params.push(toolchain.params.channelCfg.buildParams);
            }
            params.push('-logfile', '-');
            this._lastErrCode = await cmd.run(toolchain.params.unityTool!, params, {
                logPrefix: `[${tag}]`, 
                outputFormatter: cmdOption.shortOutput ? (raw) => {
                    if (outputStripper != null) raw = outputStripper(raw);
                    if (raw.startsWith('DisplayProgressbar') || raw.startsWith('[Performance]') || raw.startsWith('RefreshProfiler:')) return '';
                    const lines = raw.trim().split(/\r?\n+/);
                    if (lines[lines.length - 1].match(/\(Filename: .+\.cs Line: \d+\)/)) {
                        for (let i = 1; i < lines.length - 1; i++) {
                            if (lines[i].startsWith('UnityEditor.') || lines[i].startsWith('UnityEngine.')) {
                                lines.length = i;
                                return lines.join('\n');
                            }
                        }
                    }
                    return raw;
                } : undefined
            });
        } catch(e) {
            const errAlias = e as ExecuteError;
            this._lastErrCode = errAlias.code;
            toolchain.logParser.parseUnityBuildLog(cmd.output, this._lastErrCode);
            if(toolchain.logParser.error) {
                await sendBuildFailureAlert(toolchain.logParser.error);
            }
            process.exit(1);
        }
        
        if(!ignoreError) {
            toolchain.logParser.parseUnityBuildLog(cmd.output, this._lastErrCode);
            if(toolchain.logParser.error) {
                await sendBuildFailureAlert(toolchain.logParser.error);
                process.exit(1);
            }
        }

        if (this._lastErrCode != 0) {
            await sendBuildFailureAlert('unity返回错误码：' + this._lastErrCode);
            process.exit(1);
        }
              
        return cmd;
    }

    public async getUnityInfo(params: BuildParams): Promise<void> {        
        // 读取unity版本号
        params.unityVer = await this.identifyUnityVersion(params);
        console.log('unity version:', params.unityVer);

        // 获取unity.exe位置
        params.unityTool = params.hostCfg.unity_location?.[params.unityVer];
        if (params.unityTool == null && process.env.APPDATA) {
            // 如果没有配置，则读取UnityHub的信息查找
            const editorsJson = path.join(process.env.APPDATA, 'UnityHub/editors.json');
            const editorsJsonV2 = path.join(process.env.APPDATA, 'UnityHub/editors-v2.json');
            let unity: IUnityEditor | undefined = undefined;
            if (fs.existsSync(editorsJsonV2)) {
                const editors: IHubEditorsV2 = await fs.readJson(editorsJsonV2);
                unity = editors.data.find(v => v.version == params.unityVer);
                if (!unity) {
                    // 某些情况下不会记录，根据其它版本进行查找
                    for (const v of editors.data) {
                        const exe = v.location.find(v => v.endsWith('Unity.exe'))?.replace(v.version, params.unityVer);
                        if (exe && fs.existsSync(exe)) {
                            params.unityTool = exe;
                        }
                    }
                }
            } else if (fs.existsSync(editorsJson)) {
                const editors: IHubEditors = await fs.readJson(editorsJson);
                unity = editors[params.unityVer];
            }
            if (unity) {
                params.unityTool = unity.location.find(v => v.endsWith('Unity.exe'));
            }
        }
        if (params.unityTool == null) {
            console.error('cannot find unity location for:', params.unityVer);
            process.exit(1);
        }
        console.log('unity location:', params.unityTool);
        if (params.unityVer === '5.6.3p4') {
            // 563使用旧版android，新版某些接口已被删除
            // https://blog.csdn.net/aikb6223/article/details/102349860
            if (params.androidSdkRoot_unity563) {
                params.androidSdkRoot = params.androidSdkRoot_unity563;
            } else {
                console.warn('[!!!!!] androidSdkRoot_unity563 not provided in the hostinfo.xml');
                console.warn('[!!!!!] unity 5.6.3p4 需使用旧版Android sdk tools，请确保构建机提供了旧版Android sdk tools!');
            }
        }
    }

    private async identifyUnityVersion(params: BuildParams): Promise<string> {
        const settingContent = await fs.readFile(path.join(params.workSpacePath, 'ProjectSettings/ProjectVersion.txt'), 'utf-8');
        const mr = settingContent.match(/m_EditorVersion: (\S+)/);
        return mr![1];
    }

    public async decideAndroidEnv(): Promise<void> {
        if (toolchain.option.engine == "laya") {
            toolchain.params.projCfg.ndkTool = path.join(env.defaultNDK, 'r19');
            toolchain.params.androidNdkRoot = toolchain.params.projCfg.ndkTool || '';
            return;
        }

        // 旧版配置会在hostinfo.yml中为各个项目配置对应的ndk版本，新版已不需要配置hostinfo.yml
        const unityVer = toolchain.params.unityVer.match(/^([\d\.]+)/)![0];
        const ndkTool = toolchain.params.projCfg.ndkTool;
        if (!ndkTool && toolchain.params.projCfg.autoGenerated) {
            // 新版不需要配置hostinfo.yml，直接根据unity版本号确定ndk版本
            let ndkVer = 'r19';
            // 版本对应关系：https://docs.unity3d.com/Manual/android-sdksetup.html
            if (compareVersions(unityVer, '2022') >= 0) {
                ndkVer = 'r23b';
            } else if (compareVersions(unityVer, '2021')) {
                ndkVer = 'r21d';
            }
            toolchain.params.projCfg.ndkTool = eitherPath(path.join(env.defaultNDK, ndkVer), path.join(env.defaultNDK, `android-ndk-${ndkVer}`));
            console.log('use default ndk:', toolchain.params.projCfg.ndkTool);
        }
        toolchain.params.androidNdkRoot = toolchain.params.projCfg.ndkTool || '';
        // 经测试，2022无法通过EditorPrefs设置ndk root，需使用：https://docs.unity3d.com/ScriptReference/Android.AndroidExternalToolsSettings-ndkRootPath.html
        toolchain.params.needsetEditorPrefs = compareVersions(unityVer, '2022') < 0;
        if(!toolchain.params.androidNdkRoot) {
            toolchain.params.needsetEditorPrefs = false;
            toolchain.params.androidNdkRoot = toolchain.params.hostProjCfg.ndkTool || '';
        }
        if (toolchain.params.androidNdkRoot && !fs.existsSync(toolchain.params.androidNdkRoot)) {
            throw new Error('ndk not exists: ' + toolchain.params.androidNdkRoot);
        }
    }

    public getUnityLogFile(): string {
        const osType = os.type();
        if('Linux' == osType) {
            return '~/.config/unity3d/Editor.log';
        } else if('Darwin' == osType) {
            return '~/Library/Logs/Unity/Editor.log';
        } else {
            return `${process.env.LOCALAPPDATA}/Unity/Editor/Editor.log`;
        }
    }

    public supportCompressTex(): boolean {
        // const unityVerSim = toolchain.params.unityVer.match(/^[\d\.]+/)![0];
        // const maxVerSim = WebGLEnv.CompressTexMaxVer.match(/^[\d\.]+/)![0];
        // return compareVersions(unityVerSim, maxVerSim) <= 0;
        return true;
    }

    public getAssetsDir(): string {
        let subDir = toolchain.option.platform.toLowerCase();
        if (toolchain.option.webglRuntime == 'minigame') {
            subDir = 'wx';
        }
        return `assets/${subDir}`;
    }

    public getCDNURL(): string {
        let cdnURL = toolchain.params.channelCfg.url;
        if (!cdnURL.endsWith('/')) {
            cdnURL += '/';
        }
        cdnURL += this.getAssetsDir();
        return cdnURL;
    }

    public async dignoseEnv(): Promise<void> {
        const manifestFile = path.join(toolchain.params.workSpacePath, 'Packages/manifest.json');
        const manifest: { dependencies: Record<string, string> } = await fs.readJson(manifestFile);
        this._env = {
            ...manifest,
           puertsCoreVer: '1.4.0-rc.6',
           puertsWebglVer: '',
        }

        // 获取puerts core版本号        
        const puertsCoreRoot = this.getPuertsCoreRoot();
        const corePkgJson = path.join(puertsCoreRoot, 'package.json');
        if (fs.existsSync(corePkgJson)) {
            const pkgInfo: { version: string } = await fs.readJSON(corePkgJson);
            this._env.puertsCoreVer = pkgInfo.version;
        }

        // 获取puerts webgl版本号        
        const puertsWebglRoot = this.getPuertsWebglRoot();
        const webglPkgJson = path.join(puertsWebglRoot, 'package.json');
        if (fs.existsSync(webglPkgJson)) {
            const pkgInfo: { version: string } = await fs.readJSON(webglPkgJson);
            this._env.puertsWebglVer = pkgInfo.version;
        }
    }

    public isPuertsInstalledViaLocalUPM(packageName: EPuertsPackage): boolean {
        if (this._env == null) throw new Error('dignoseEnv not called!')
        return typeof(this._env.dependencies[packageName]) == 'string';
    }

    public getPuertsCoreRoot(): string {
        if (this._env == null) throw new Error('dignoseEnv not called!');
        const local = this._env.dependencies[EPuertsPackage.Core];
        if (local) {
            return path.join(toolchain.params.workSpacePath, 'Packages', local.replace(/^file:/, ''));
        } else {
            return path.join(toolchain.params.workSpacePath, 'Assets/Puerts');
        }
    }

    public getPuertsCommonjsInstallRoot(): string {
        if (this._env == null) throw new Error('dignoseEnv not called!');
        const local = this._env.dependencies[EPuertsPackage.CommonJS];
        if (local) {
            return path.join(toolchain.params.workSpacePath, 'Packages', local.replace(/^file:/, ''));
        } else {
            return path.join(toolchain.params.workSpacePath, 'Assets/Puerts');
        }
    }

    public getPuertsWebglRoot(): string {
        if (this._env == null) throw new Error('dignoseEnv not called!');
        const local = this._env.dependencies[EPuertsPackage.WebGL];
        if (local) {
            return path.join(toolchain.params.workSpacePath, 'Packages', local.replace(/^file:/, ''));
        } else {
            return path.join(toolchain.params.workSpacePath, 'Assets/Puerts_webgl');
        }
    }

    public getPuertsCoreVer(): string {
        if (this._env == null) throw new Error('dignoseEnv not called!');
        return this._env.puertsCoreVer;
    }

    public async commitFullBuildVer(ver: string): Promise<void> {
        this.fullVersion = ver;
    }

    public async getResVersion(): Promise<number> {
        // 注意
        const assetsDir = this.getAssetsDir();
        const resversionfile = path.join(toolchain.params.uploadPath, assetsDir, 'version.txt');
        if (!fs.existsSync(resversionfile)) {
            // 首次构建小游戏，在压缩贴图前，不会将资源上传到资源服务器，此时没有version.txt，旧版本号视为0
            return 0;
        }
        const [resVer] =  await readVersionTxt(resversionfile);
        return Number(resVer);
    }

    /**获取本次构建相关的完整版本号 */
    public async getFullBuildVer(): Promise<string> {
        if (this.fullVersion) return this.fullVersion;

        // 抖音版本号格式采用 主版本号.子版本号.修正版本号 的格式，不包含资源版本号信息，资源版本号已写入ResLoader.cs
        if (toolchain.option.webglRuntime == 'douyin') {
            const prebuildResult = toolchain.taskHelper.getResult<PrebuildAppData>(PreBuildAppTask.name);
            if (prebuildResult != null) {
                return (toolchain.params.channelCfg.bundleVersion ?? toolchain.params.version) + '.' + prebuildResult.data!.versionCode;
            }
            // 说明跳过了pre build app步骤，可能是只执行资源外发，这种情况下读取appVer.txt保存的版本信息
            const appVerFile = path.join(toolchain.params.workSpacePath, WebGLEnv.DouyinAppVerFile);
            return await fs.readFile(appVerFile, 'utf-8');            
        }
        
        // 读取资源版本号
        const resVer =  await this.getResVersion();

        // if (toolchain.option.platform == 'Android' && toolchain.option.buildExe) {
        //     const appRst = toolchain.taskHelper.getResult<IBuildAppResult>(AndroidBuilder.name);
        //     if (appRst?.data) {
        //         // 如果cfg.json中配置了bundleVersion，此时appRst.data.version将为bundleVersopm(buildVersion)
        //         return `${appRst.data.version}r${resVer}`;
        //     }
        // }
        // 读取主版本号
        const verTxt = path.join(toolchain.params.workSpacePath, CommonEnv.VersionCodeTxt);
        const mainVer = await fs.readFile(verTxt, 'utf-8');
        const appVer = `${toolchain.params.version}.${mainVer}`;
        return `${appVer}(${resVer})`;
    }

    public async readGUID(metaFile: string): Promise<string> {
        const content = await fs.readFile(metaFile, 'utf-8');
        const result = content.match(/guid: (\w{32})/);
        if (result == null) {
            console.error('Cannot parse guid from:', metaFile);
            return '';
        }
        return result[1];
    }

    /**
     * 查询所有AB包文件的版本号信息
     * @param files 
     * @returns 
     */
    public async getABFileMD5s(): Promise<Record<string, string>> {
        const abTxt = path.join(toolchain.params.workSpacePath, CommonEnv.PublishAssets, toolchain.option.platform.toLowerCase(), 'abMD5.txt');
        if (!fs.existsSync(abTxt)) {
            await sendBuildFailureAlert('abMD5.txt not found: ' + abTxt);
            process.exit(1);
        }

        const abMD5Content = await fs.readFile(abTxt, 'utf-8');
        const lines = abMD5Content.split(/\r?\n+/);
        const hash: Record<string, string> = {};
        for (const line of lines) {
            if (!line) continue;
            const [file, originalMD5, md5] = line.split('|');
            hash[file] = md5.substring(0, 10);
        }
        
        return hash;
    }
}
